"""
DjangoBlog 核心数据模型模块
定义博客系统的所有数据表结构，包括文章、分类、标签等核心业务模型
"""

import logging
from abc import abstractmethod

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField # type: ignore
from uuslug import slugify # type: ignore

from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site

# 配置日志记录器，用于记录程序运行时的信息
logger = logging.getLogger(__name__)


class LinkShowType(models.TextChoices):
    """
    链接展示类型选择类
    定义友情链接在不同页面的展示方式
    """
    I = ('i', _('首页'))  # 仅在首页显示
    L = ('l', _('列表页'))  # 在文章列表页显示
    P = ('p', _('文章页'))  # 在文章详情页显示
    A = ('a', _('所有页面'))  # 在所有页面显示
    S = ('s', _('轮播图'))  # 以轮播图形式显示


class BaseModel(models.Model):
    """
    抽象基类模型
    为所有模型提供公共字段和方法，实现代码复用
    """
    
    # 主键字段 - 自增ID，作为每条记录的唯一标识
    id = models.AutoField(primary_key=True)
    
    # 创建时间 - 记录对象创建的时间，默认使用当前时间
    creation_time = models.DateTimeField(_('创建时间'), default=now)
    
    # 最后修改时间 - 记录对象最后修改的时间，默认使用当前时间
    last_modify_time = models.DateTimeField(_('修改时间'), default=now)

    def save(self, *args, **kwargs):
        """
        重写保存方法，添加自定义逻辑
        处理文章浏览量更新和slug字段自动生成
        """
        # 检查是否是更新文章浏览量的操作
        is_update_views = isinstance(
            self,
            Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
        
        if is_update_views:
            # 如果是更新浏览量，直接使用update方法提高性能
            Article.objects.filter(pk=self.pk).update(views=self.views) # type: ignore
        else:
            # 自动生成slug字段：如果模型有title就用title，否则用name
            if 'slug' in self.__dict__:
                slug = getattr(
                    self, 'title') if 'title' in self.__dict__ else getattr(
                    self, 'name')
                setattr(self, 'slug', slugify(slug))
            
            # 调用父类的保存方法完成实际保存操作
            super().save(*args, **kwargs)

    def get_full_url(self):
        """
        获取对象的完整URL地址
        包含域名和路径，用于生成外部可访问的链接
        """
        site = get_current_site().domain  # 获取当前网站的域名
        url = "https://{site}{path}".format(site=site,
                                            path=self.get_absolute_url())
        return url

    class Meta:
        """元配置类，标记此为抽象基类，不会在数据库中创建表"""
        abstract = True

    @abstractmethod
    def get_absolute_url(self):
        """
        抽象方法 - 获取对象的绝对URL
        子类必须实现此方法，返回对象的详情页URL
        """
        pass


class Article(BaseModel):
    """
    文章模型类
    博客系统的核心模型，存储所有文章相关内容
    继承自BaseModel，获得公共字段和方法
    """
    
    # 文章状态选择项 - 控制文章的可见性
    STATUS_CHOICES = (
        ('d', _('草稿')),  # 草稿状态，仅作者可见
        ('p', _('已发布')),  # 已发布状态，所有用户可见
    )
    
    # 评论状态选择项 - 控制文章是否允许评论
    COMMENT_STATUS = (
        ('o', _('开启')),  # 开启评论功能
        ('c', _('关闭')),  # 关闭评论功能
    )
    
    # 文章类型选择项 - 区分普通文章和页面
    TYPE = (
        ('a', _('文章')),  # 普通博客文章
        ('p', _('页面')),  # 静态页面（如关于页面）
    )
    
    # 文章标题 - 字符字段，最大长度200，必须唯一
    title = models.CharField(_('标题'), max_length=200, unique=True)
    
    # 文章正文 - 使用Markdown编辑器字段，支持富文本编辑
    body = MDTextField(_('正文'))
    
    # 发布时间 - 文章正式发布的时间
    pub_time = models.DateTimeField(
        _('发布时间'), blank=False, null=False, default=now)
    
    # 文章状态 - 选择字段，控制文章是草稿还是已发布
    status = models.CharField(
        _('状态'),
        max_length=1,
        choices=STATUS_CHOICES,
        default='p')  # 默认设置为已发布
    
    # 评论状态 - 选择字段，控制是否允许评论
    comment_status = models.CharField(
        _('评论状态'),
        max_length=1,
        choices=COMMENT_STATUS,
        default='o')  # 默认开启评论
    
    # 文章类型 - 选择字段，区分文章和页面
    type = models.CharField(_('类型'), max_length=1, choices=TYPE, default='a')  # 默认为文章
    
    # 浏览量 - 正整数字段，记录文章被查看的次数
    views = models.PositiveIntegerField(_('浏览量'), default=0)
    
    # 作者 - 外键关联用户模型，文章删除时级联删除
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_('作者'),
        blank=False,
        null=False,
        on_delete=models.CASCADE)
    
    # 文章排序 - 整数字段，用于自定义文章显示顺序
    article_order = models.IntegerField(
        _('排序'), blank=False, null=False, default=0)
    
    # 显示目录 - 布尔字段，控制是否显示文章目录
    show_toc = models.BooleanField(_('显示目录'), blank=False, null=False, default=False)
    
    # 分类 - 外键关联分类模型，一篇文章属于一个分类
    category = models.ForeignKey(
        'Category',
        verbose_name=_('分类'),
        on_delete=models.CASCADE,
        blank=False,
        null=False)
    
    # 标签 - 多对多关联标签模型，一篇文章可以有多个标签
    tags = models.ManyToManyField('Tag', verbose_name=_('标签'), blank=True)

    def body_to_string(self):
        """将文章正文转换为字符串，主要用于序列化或其他处理"""
        return self.body

    def __str__(self):
        """定义对象的字符串表示，在Admin后台等处显示"""
        return self.title

    class Meta:
        """文章模型的元配置"""
        ordering = ['-article_order', '-pub_time']  # 默认按排序和发布时间降序排列
        verbose_name = _('文章')  # 在Admin后台显示的单数名称
        verbose_name_plural = verbose_name  # 在Admin后台显示的复数名称
        get_latest_by = 'id'  # 获取最新记录的依据字段

    def get_absolute_url(self):
        """
        获取文章的绝对URL路径
        用于生成文章详情页的链接，包含年月日信息用于SEO
        """
        return reverse('blog:detailbyid', kwargs={
            'article_id': self.id,
            'year': self.creation_time.year,
            'month': self.creation_time.month,
            'day': self.creation_time.day
        })

    @cache_decorator(60 * 60 * 10)  # 缓存装饰器，缓存10小时
    def get_category_tree(self):
        """
        获取文章所属分类的完整层级树
        返回包含所有父级分类的列表，用于面包屑导航等场景
        """
        tree = self.category.get_category_tree()  # 调用分类的方法获取层级树
        names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))  # 提取分类名称和URL

        return names

    def save(self, *args, **kwargs):
        """重写保存方法，调用父类的保存逻辑"""
        super().save(*args, **kwargs)

    def viewed(self):
        """
        增加文章浏览量
        每次调用浏览量加1，使用update_fields优化性能
        """
        self.views += 1
        self.save(update_fields=['views'])  # 只更新views字段

    def comment_list(self):
        """
        获取文章的有效评论列表
        使用缓存优化性能，避免频繁查询数据库
        """
        cache_key = 'article_comments_{id}'.format(id=self.id)  # 构建缓存键
        value = cache.get(cache_key)  # 尝试从缓存获取
        
        if value:
            # 缓存命中，记录日志并返回缓存数据
            logger.info('get article comments:{id}'.format(id=self.id))
            return value
        else:
            # 缓存未命中，从数据库查询并设置缓存
            comments = self.comment_set.filter(is_enable=True).order_by('-id')  # type: ignore # 获取有效评论并按ID降序
            cache.set(cache_key, comments, 60 * 100)  # 设置缓存，有效期100分钟
            logger.info('set article comments:{id}'.format(id=self.id))
            return comments

    def get_admin_url(self):
        """获取文章在Django Admin后台的编辑URL"""
        info = (self._meta.app_label, self._meta.model_name)  # 获取应用名和模型名
        return reverse('admin:%s_%s_change' % info, args=(self.pk,))  # 生成Admin编辑URL

    @cache_decorator(expiration=60 * 100)  # 缓存100分钟
    def next_article(self):
        """
        获取下一篇文章
        按ID顺序查找当前文章之后的第一篇已发布文章
        """
        return Article.objects.filter(
            id__gt=self.id, status='p').order_by('id').first()

    @cache_decorator(expiration=60 * 100)  # 缓存100分钟
    def prev_article(self):
        """
        获取上一篇文章
        按ID倒序查找当前文章之前的第一篇已发布文章
        """
        return Article.objects.filter(id__lt=self.id, status='p').first()


class Category(BaseModel):
    """
    文章分类模型
    支持多级分类，用于对文章进行分类管理
    """
    
    # 分类名称 - 必须唯一，用于标识分类
    name = models.CharField(_('分类名称'), max_length=30, unique=True)
    
    # 父级分类 - 自关联外键，实现多级分类结构
    parent_category = models.ForeignKey(
        'self',
        verbose_name=_('父级分类'),
        blank=True,
        null=True,
        on_delete=models.CASCADE)  # 允许为空，表示顶级分类
    
    # 分类别名 - 用于URL中的友好标识
    slug = models.SlugField(default='no-slug', max_length=60, blank=True)
    
    # 排序索引 - 控制分类的显示顺序
    index = models.IntegerField(default=0, verbose_name=_('排序'))

    class Meta:
        """分类模型的元配置"""
        ordering = ['-index']  # 按索引降序排列
        verbose_name = _('分类')  # 单数显示名称
        verbose_name_plural = verbose_name  # 复数显示名称

    def get_absolute_url(self):
        """获取分类页面的绝对URL，使用slug作为URL参数"""
        return reverse(
            'blog:category_detail', kwargs={
                'category_name': self.slug})

    def __str__(self):
        """返回分类名称作为字符串表示"""
        return self.name

    @cache_decorator(60 * 60 * 10)  # 缓存10小时
    def get_category_tree(self):
        """
        递归获取分类的完整父级层级树
        返回从当前分类到根分类的所有分类列表
        """
        categorys = []  # 存储分类层级列表

        def parse(category):
            """
            内部递归函数，遍历分类层级
            """
            categorys.append(category)  # 将当前分类加入列表
            if category.parent_category:
                parse(category.parent_category)  # 递归处理父级分类

        parse(self)  # 从当前分类开始递归
        return categorys

    @cache_decorator(60 * 60 * 10)  # 缓存10小时
    def get_sub_categorys(self):
        """
        获取当前分类的所有子级分类
        包括直接子分类和间接子分类
        """
        categorys = []  # 存储所有子分类
        all_categorys = Category.objects.all()  # 获取所有分类

        def parse(category):
            """
            内部递归函数，遍历子分类
            """
            if category not in categorys:
                categorys.append(category)  # 添加当前分类到结果列表
            childs = all_categorys.filter(parent_category=category)  # 查找直接子分类
            for child in childs:
                if category not in categorys:
                    categorys.append(child)  # 添加子分类到结果列表
                parse(child)  # 递归处理子分类的子分类

        parse(self)  # 从当前分类开始递归
        return categorys


class Tag(BaseModel):
    """
    文章标签模型
    用于对文章进行标签化分类，一篇文章可以有多个标签
    """
    
    # 标签名称 - 必须唯一
    name = models.CharField(_('标签名称'), max_length=30, unique=True)
    
    # 标签别名 - 用于URL中的友好标识
    slug = models.SlugField(default='no-slug', max_length=60, blank=True)

    def __str__(self):
        """返回标签名称作为字符串表示"""
        return self.name

    def get_absolute_url(self):
        """获取标签页面的绝对URL，使用slug作为URL参数"""
        return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})

    @cache_decorator(60 * 60 * 10)  # 缓存10小时
    def get_article_count(self):
        """获取使用该标签的文章数量，使用distinct去重"""
        return Article.objects.filter(tags__name=self.name).distinct().count()

    class Meta:
        """标签模型的元配置"""
        ordering = ['name']  # 按名称升序排列
        verbose_name = _('标签')  # 单数显示名称
        verbose_name_plural = verbose_name  # 复数显示名称


class Links(models.Model):
    """
    友情链接模型
    管理网站的外部友情链接
    """
    
    # 链接名称 - 必须唯一
    name = models.CharField(_('链接名称'), max_length=30, unique=True)
    
    # 链接地址 - URL字段，存储外部链接
    link = models.URLField(_('链接地址'))
    
    # 显示顺序 - 控制链接的显示位置，必须唯一
    sequence = models.IntegerField(_('排序'), unique=True)
    
    # 是否启用 - 控制链接是否显示
    is_enable = models.BooleanField(
        _('是否显示'), default=True, blank=False, null=False)
    
    # 显示类型 - 选择字段，控制链接在哪些页面显示
    show_type = models.CharField(
        _('显示类型'),
        max_length=1,
        choices=LinkShowType.choices,
        default=LinkShowType.I)  # 默认在首页显示
    
    # 创建时间 - 记录链接创建的时间
    creation_time = models.DateTimeField(_('创建时间'), default=now)
    
    # 最后修改时间 - 记录链接最后修改的时间
    last_mod_time = models.DateTimeField(_('修改时间'), default=now)

    class Meta:
        """友情链接的元配置"""
        ordering = ['sequence']  # 按排序字段升序排列
        verbose_name = _('友情链接')  # 单数显示名称
        verbose_name_plural = verbose_name  # 复数显示名称

    def __str__(self):
        """返回链接名称作为字符串表示"""
        return self.name


class SideBar(models.Model):
    """
    侧边栏模型
    管理网站侧边栏的自定义内容，支持HTML
    """
    
    # 侧边栏标题 - 显示名称
    name = models.CharField(_('标题'), max_length=100)
    
    # 侧边栏内容 - 文本字段，可以包含HTML代码
    content = models.TextField(_('内容'))
    
    # 显示顺序 - 控制侧边栏模块的显示位置，必须唯一
    sequence = models.IntegerField(_('排序'), unique=True)
    
    # 是否启用 - 控制侧边栏模块是否显示
    is_enable = models.BooleanField(_('是否启用'), default=True)
    
    # 创建时间 - 记录侧边栏创建的时间
    creation_time = models.DateTimeField(_('创建时间'), default=now)
    
    # 最后修改时间 - 记录侧边栏最后修改的时间
    last_mod_time = models.DateTimeField(_('修改时间'), default=now)

    class Meta:
        """侧边栏的元配置"""
        ordering = ['sequence']  # 按排序字段升序排列
        verbose_name = _('侧边栏')  # 单数显示名称
        verbose_name_plural = verbose_name  # 复数显示名称

    def __str__(self):
        """返回侧边栏名称作为字符串表示"""
        return self.name


class BlogSettings(models.Model):
    """
    博客配置模型
    存储博客系统的全局配置信息，采用单例模式
    """
    
    # 网站名称 - 博客的名称
    site_name = models.CharField(
        _('网站名称'),
        max_length=200,
        null=False,
        blank=False,
        default='')
    
    # 网站描述 - 博客的简短描述
    site_description = models.TextField(
        _('网站描述'),
        max_length=1000,
        null=False,
        blank=False,
        default='')
    
    # SEO描述 - 用于搜索引擎优化的描述
    site_seo_description = models.TextField(
        _('SEO描述'), max_length=1000, null=False, blank=False, default='')
    
    # 网站关键词 - 用于搜索引擎优化的关键词
    site_keywords = models.TextField(
        _('网站关键词'),
        max_length=1000,
        null=False,
        blank=False,
        default='')
    
    # 文章摘要长度 - 控制文章列表页显示摘要的长度
    article_sub_length = models.IntegerField(_('文章摘要长度'), default=300)
    
    # 侧边栏文章数量 - 控制侧边栏最新文章显示数量
    sidebar_article_count = models.IntegerField(_('侧边栏文章数量'), default=10)
    
    # 侧边栏评论数量 - 控制侧边栏最新评论显示数量
    sidebar_comment_count = models.IntegerField(_('侧边栏评论数量'), default=5)
    
    # 文章页评论数量 - 控制文章详情页评论显示数量
    article_comment_count = models.IntegerField(_('文章页评论数量'), default=5)
    
    # 是否显示Google广告 - 控制广告显示开关
    show_google_adsense = models.BooleanField(_('显示广告'), default=False)
    
    # 广告代码 - 存储Google Adsense等广告代码
    google_adsense_codes = models.TextField(
        _('广告代码'), max_length=2000, null=True, blank=True, default='')
    
    # 是否开启全站评论 - 全局控制评论功能
    open_site_comment = models.BooleanField(_('开启全站评论'), default=True)
    
    # 公共头部代码 - 全站公共的头部HTML代码
    global_header = models.TextField("公共头部", null=True, blank=True, default='')
    
    # 公共尾部代码 - 全站公共的尾部HTML代码
    global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
    
    # 备案号 - 网站ICP备案号
    beian_code = models.CharField(
        '备案号',
        max_length=2000,
        null=True,
        blank=True,
        default='')
    
    # 网站统计代码 - 如百度统计、Google Analytics代码
    analytics_code = models.TextField(
        "网站统计代码",
        max_length=1000,
        null=False,
        blank=False,
        default='')
    
    # 是否显示公安备案号 - 控制公安备案号显示
    show_gongan_code = models.BooleanField(
        '是否显示公安备案号', default=False, null=False)
    
    # 公安备案号 - 网站公安备案号
    gongan_beiancode = models.TextField(
        '公安备案号',
        max_length=2000,
        null=True,
        blank=True,
        default='')
    
    # 评论是否需要审核 - 控制评论是否需审核后显示
    comment_need_review = models.BooleanField(
        '评论是否需要审核', default=False, null=False)

    class Meta:
        """博客配置的元配置"""
        verbose_name = _('网站配置')  # 单数显示名称
        verbose_name_plural = verbose_name  # 复数显示名称

    def __str__(self):
        """返回网站名称作为字符串表示"""
        return self.site_name

    def clean(self):
        """
        数据清洗方法，确保配置记录的唯一性
        使用单例模式，只允许存在一条配置记录
        """
        if BlogSettings.objects.exclude(id=self.id).count(): # type: ignore
            raise ValidationError(_('只能有一条配置记录'))  # 如果已存在其他配置则抛出异常

    def save(self, *args, **kwargs):
        """
        重写保存方法，保存后清空缓存
        确保配置更改后立即生效
        """
        super().save(*args, **kwargs)
        from djangoblog.utils import cache
        cache.clear()  # 清除所有缓存，使新配置立即生效